package fr.umlv.m2.jee.spij.service.unit.impl;

import fr.umlv.m2.jee.spij.persistence.education.Unit;
import fr.umlv.m2.jee.spij.persistence.education.dao.HibernateUnitDAO;
import fr.umlv.m2.jee.spij.service.unit.DisplayUnit;
import fr.umlv.m2.jee.spij.service.unit.IDisplayUnitService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.binding.message.MessageBuilder;
import org.springframework.binding.message.MessageContext;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import javax.faces.model.SelectItem;
import java.util.ArrayList;
import java.util.List;

/**
 * Provides default implementation of {@code IDisplayUnitService}.
 *
 * @author thomas magny
 * @author mathieu mazzolini
 */
@Service("defaultDisplayUnitService")
public class DefaultDisplayUnitService implements IDisplayUnitService {

    /**
     * the DAO.
     */
    @Autowired
    private HibernateUnitDAO unitDAO;

    /**
     * Finds units into the database,
     * then create a list of DTO unit and create one DTO for each unit founded.
     * The method add a message in the message context
     * if no unit has been founded.
     * This message is an info message with the code 'units.empty'.
     * 'units.empty' must be provided
     * in messages resources of flows to be displayed.
     *
     * @param context the message context.
     *
     * @return a list of {@code DisplayUnit}.
     */
    @Override
    @Transactional(readOnly = true)
    public final List<DisplayUnit> getAllDisplayUnit(
            final MessageContext context) {
        final List<Unit> unitList = unitDAO.findAll();
        final List<DisplayUnit> displayUnitList = new ArrayList<DisplayUnit>();

        if (unitList == null) {
            context.addMessage(
                    new MessageBuilder().info().code("units.empty").build());
        } else {
            for (Unit unit : unitList) {
                displayUnitList.add(new DisplayUnit(unit));
            }
        }
        return displayUnitList;
    }

    /**
     * ...
     *
     * @param context the message context.
     *
     * @return a list of selectItem.
     */
    @Override
    @Transactional(readOnly = true)
    public final List<SelectItem> getAllDisplayUnitSelectItem(
            final MessageContext context) {
        final List<Unit> unitList = unitDAO.findAll();
        List<SelectItem> selectUnits =
                new ArrayList<SelectItem>(unitList.size());

        if (unitList == null) {
            context.addMessage(
                    new MessageBuilder().info().code("units.empty").build());
        } else {
            for (Unit unit : unitList) {
                selectUnits.add(new SelectItem(unit.getName(), unit.getName()));
            }
        }
        return selectUnits;
    }

    /**
     * Finds a unit by its id then creates a {@code DisplayUnit} from the unit.
     * This adds a message into the message context
     * if the unit with its id does not exist.
     * The message is an error message style with the code 'unit.empty'.
     *
     * @param id      the id which is the primary key of unit into the database.
     * @param context the message context.
     *
     * @return a {@code DisplayUnit} object.
     */
    @Override
    @Transactional(readOnly = true)
    public final DisplayUnit getUnitById(final long id,
                                         final MessageContext context) {
        final Unit unit = unitDAO.findById(id);
        if (unit == null) {
            context.addMessage(
                    new MessageBuilder().error().code("unit.empty").build());
        }
        return new DisplayUnit(unit);
    }

    /**
     * Persists a new unit into the database.
     * Sets the unit field with the displayUnit field is they are filled.
     * An error message occur if the credit given by the user is negative,
     * in this case, the unit is not created.
     * An error message can occur
     * if the unit name already exist into the database.
     *
     * @param displayUnit the object that stores the data to persist.
     * @param context     the message context.
     *
     * @return true if the creation succeed, false otherwise due to errors.
     */
    @Override
    @Transactional(readOnly = false)
    public final boolean createUnit(final DisplayUnit displayUnit,
                                    final MessageContext context) {
        final Unit unit = new Unit();

        if (!displayUnit.getName().isEmpty()) {
            unit.setName(displayUnit.getName());
        }
        if (displayUnit.getCredit() > 0) {
            unit.setCredit(displayUnit.getCredit());
        } else {

            // TODO validator dans la page plutot qu'ici ?
            context.addMessage(new MessageBuilder()
                    .error().code("unit.credit.positive").build());
            return false;
        }

        // Checks whether a unit already exists
        for (Unit persistUnit : unitDAO.findAll()) {
            if (persistUnit.getName().equals(unit.getName())) {
                context.addMessage(new MessageBuilder().error()
                        .arg(unit.getName()).code("unit.name.unique").build());
                return false;
            }
        }

        unitDAO.persist(unit);
        context.addMessage(new MessageBuilder().info()
                .arg(unit.getName()).code("unit.persist").build());

        return true;
    }


    /**
     * Removes a unit by its id.
     * An info message is added if the unit is deleted,
     * its displays the name of the unit deleted.
     *
     * @param id      the unit id to remove.
     * @param context the message context.
     *
     * @return true if the unit has been removed correctly, false otherwise.
     */
    @Override
    @Transactional(readOnly = false)
    public final boolean deleteUnitById(final long id,
                                        final MessageContext context) {
        boolean hasBeenRemoved = false;
        final Unit unit = unitDAO.findById(id);

        if (unit != null) {
            context.addMessage(new MessageBuilder().info()
                    .arg(unit.getName()).code("unit.delete").build());
            unitDAO.remove(unit);
            hasBeenRemoved = true;
        }

        return hasBeenRemoved;
    }

    /**
     * Updates an unit.
     * Retrieves the unit into the database
     * with the id field of modifiedUnit argument.
     * Checks the credit given, adds a message error
     * if the credit is negative.
     * Checks that the name and the credit is not the same
     * that the unit database (no update necessary)
     * Adds informative messages into the context in case of success
     * to display the update (before/after value).
     *
     * @param modifiedUnit the data to merge.
     * @param context      the message context.
     *
     * @return true if the update happens, false otherwise.
     */
    @Override
    @Transactional(readOnly = false)
    public final boolean updateUnitById(final DisplayUnit modifiedUnit,
                                        final MessageContext context) {
        boolean hasBeenUpdated = false;
        final Unit toMerge = unitDAO.findById(modifiedUnit.getId());

        // TODO better validators
        if (modifiedUnit.getCredit() <= 0) {
            context.addMessage(new MessageBuilder().error()
                    .code("unit.credit.positive").build());
            return false;
        }

        if (toMerge != null) {
            if (!toMerge.getName().equals(modifiedUnit.getName())) {
                context.addMessage(new MessageBuilder().info()
                        .args(
                                new Object[]{toMerge.getName(),
                                        modifiedUnit.getName()})
                        .code("unit.update.name").build());
                toMerge.setName(modifiedUnit.getName());
                hasBeenUpdated = true;
            }
            if (toMerge.getCredit() != modifiedUnit.getCredit()) {
                context.addMessage(new MessageBuilder().info()
                        .args(
                                new Object[]{toMerge.getCredit(),
                                        modifiedUnit.getCredit()})
                        .code("unit.update.credit").build());
                toMerge.setCredit(modifiedUnit.getCredit());
                hasBeenUpdated = true;
            }

            unitDAO.merge(toMerge);
        }

        if (!hasBeenUpdated) {
            context.addMessage(new MessageBuilder()
                    .error().defaultText("update failed").build());
        }

        return hasBeenUpdated;
    }

}
